06 · Deep Research:让一群 Agent 帮你写一份带引用的报告
零、开始之前
前置:前五篇都要读过 —— 本篇没有新范式,它是前五个的组合。
产出:一个六节点研究系统的设计,以及每个环节在防什么。
本篇有一份别处没有的材料:Anthropic 把线上 Research 功能的提示词原文开源了(MIT,在 claude-cookbooks 里)。那份 23KB 的编排提示词把「派几个人、每人几次工具调用、什么时候收手」全写成了明文,读它比读任何代码都值。
一、先看一个真实问题
你要写一份报告:
「2026 年主流 Agent 框架的选型对比,要有数据支撑,要能引用来源。」
用 01 ReAct 做:搜十几次,上下文爆了(05 第一节讲过)。
用 05 Supervisor 做:派三个子 Agent 分别查三个框架,上下文问题解决了,并行也有了。
但你会发现报告还是不能用,因为缺两样东西:
| 缺什么 | 具体表现 |
|---|---|
| 没有引用 | 报告里说「LangGraph 有 4 万星」,你没法验证这是查到的还是编的 |
| 信息损耗 | 子 Agent 返回的结论太粗,写报告时发现关键细节没了,只能再派一轮 |
Deep Research 就是在 Supervisor 的基础上,补上这两块。
二、概念:它是前五个范式的叠加
先看整体形状:
Anthropic 公开的官方架构图,与上图一一对应:

图 6-1 主管(LeadResearcher)协调多个子代理并行探索不同研究方向
图片来源:Anthropic — How we built our multi-agent research system
对照前五篇看,只有黄色那两块是新的:
| 组件 | 来自哪一篇 |
|---|---|
| 写研究简报 | 03 Plan 的规划阶段 |
| 主管派活 | 05 Supervisor |
| 子研究员内部循环 | 01 ReAct |
| 并行 | 02 Workflow 的并行模式 |
| 「还有缺口就再派一轮」 | 04 Reflection |
| ③ 压缩 | Deep Research 特有 |
| ⑤ 引用 | Deep Research 特有 |
所以这一篇的重点,就是搞懂这两个新环节,以及主管的派活策略。
2.1 它和 03 Plan-and-Execute 到底差在哪
这两个最容易 混,因为都是「先出计划再执行」。区别在于计划是给谁看的:
| 03 Plan-and-Execute | 06 Deep Research | |
|---|---|---|
| 计划是什么 | 给自己看的待办清单 | 给别人看的派活书 |
| 谁执行 | 同一个 Agent,自己逐条做 | 一批子代理,每人领一条 |
| 执行方式 | 串行 | 并行 |
| 上下文 | 全在一份历史里 | 每个子代理一份,互相看不见 |
| 计划的粒度 | 「改 A 文件」「跑测试」——动作 | 「研究 A 库的性能,用这些信源,输出事实清单」——带边界和验收标准的任务书 |
| 解决的问题 | 别忘了目标(防漂移) | 一个上下文装不下(防爆) |
| 结果怎么回来 | 就在当前历史里 | 压缩后回传,原始材料丢弃 |
一句话:03 的计划是提醒自己,06 的计划是指挥别人。
所以判断该用哪个很简单 —— 问一句:这些子任务能不能同时干?
- 不能(改文件有依赖、必须按顺序)→ 03,加了并行也没用,只多花协调成本
- 能(查五个库互不相干)→ 06
还有一个信号:如果做完一步产生的中间材料,后面几步根本用不上(比如查资料的原始网页),那就该用 06 —— 让它在子代理的上下文里烧掉,别污染主线。
三、拆 Anthropic 的生产提示词
三个文件,在 claude-cookbooks/patterns/agents/prompts/:
| 文件 | 大小 | 角色 |
|---|---|---|
research_lead_agent.md | 23KB | 主管 |
research_subagent.md | 9.1KB | 子研究员 |
citations_agent.md | 2.9KB | 引用代理 |
3.1 主管做的第一件事:判题型
主管拿到任务后,不是马上派活,而是先判断这是哪一类问题。因为不同题型的派活方式完全不同。
| 题型 | 什么样的问题 | 怎么派 |
|---|---|---|
| Depth-first 深度优先 | 一个问题,需要多个角度来看 | 派多个子代理,各自用不同的方法论/视角攻同一个问题 |
| Breadth-first 广度优先 | 能拆成互相独立的子问题 | 每个子问题一个子代理,天然并行 |
| Straightforward 直球 | 单点事实查询 | 1 个子代理就够 |
原文给的例子很有辨识度:
- 「抑郁症最有效的治疗方法是什么」→ 深度优先(多种疗法、多个学派的视角)
- 「比较三个北欧国家的经济体制」→ 广度优先(三个国家可以独立研究)
- 「东京现在人口多少」→ 直球
这个分类是整个编排的分叉点。 多数自建 Deep Research 系统的问题就出在这儿:不管什么问题都用同一套派活策略。
3.2 派几个人:有一张明确的数字表
这是最实用的一段。Anthropic 直接给了数量指南:
| 复杂度 | 子代理数 | 例子 |
|---|---|---|
| 简单 | 1 | 「今年报税截止日是哪天」 |
| 标准 | 2-3 | 「比较三大云厂商」→ 一家一个 |
| 中等 | 3-5 | 「分析 AI 对医疗的影响」→ 监管/临床/经济/技术四个面 |
| 高 | 5-10(硬上限 20) | 「财富 500 强 CEO 的出生地和年龄」→ 10 个代理各管 50 个 |
三条附加规则,每条都在防一种具体的浪费:
① 「即使是最简单的查询,也总是至少创建 1 个子代理」
这条反直觉:查个人口数字,主管自己搜一下不就完了?
但原文的理由是「以确保正确收集来源」—— 主管自己搜的话,就没有独立的信息采集轨迹,下游的引用环节会挂不上。这是为了 ⑤ 而付的固定成本。
② 「绝不要超过 20 个子代理,除非绝对必要」
原文的 措辞值得原样记住:
如果一个任务看起来需要超过 20 个子代理,通常意味着你应该重构方法……优先选择更少、更强的子代理,而不是许多过于狭窄的。更多子代理 = 更多开销。
③ 「避免子代理之间重叠」
重叠是并行编排里最贵的浪费:三个代理搜了同一批网页,你付了三份钱,拿到一份信息。
3.3 派活时必须说清楚的七件事
主管给子代理的任务描述,必须包含:
- 具体研究目标 —— 理想情况下每个子代理只有 1 个核心目标
- 期望的输出格式 —— 实体列表?事实报告?还是回答某个具体问题
- 背景上下文 —— 用户的问题是什么,这个子代理在整体计划里的位置
- 要回答的关键问题
- 建议的起点和信源 —— 什么算高质量来源,哪些来源不可信要避开
- 该用哪些工具 —— 网页搜索?还是内部的 Drive / Gmail / Slack
- 精确的范围边界 —— 防止研究漂移
第 7 条正好对应 05 的排查表里那条「派出去干 A,回来交了 B」。
Anthropic 还在提示词里放了一个完整的正面范例,是关于半导体供应链的,足足一百多个词,具体到「去 SEC EDGAR 数据库查」「优先原始信源而非新闻聚合」「重点关注当 前瓶颈和新厂产能预测」。
这个范例本身就说明了标准有多高。 你随手写的「研究一下 X」离这个差得非常远 —— 这大概是自建系统效果差的头号原因。
3.4 子研究员:预算是硬性的
子代理那份 9.1KB 的提示词,核心是给它一个明确的工具调用预算:
| 任务难度 | 允许的工具调用次数 |
|---|---|
| 简单(「今年报税截止日」) | < 5 |
| 中等 | 5 |
| 困难 | ~10 |
| 非常困难 / 多部分 | 最多 15 |
然后是硬上限:
为防止系统过载,要求你保持在 20 次工具调用和约 100 个来源以内。这是绝对最大上限。如果超过这个限制,子代理将被终止。
注意这是两层保险:
为什么要两层?因为只靠提示词约束,模型一定会超。它总觉得再查一条就更全了。
子代理内部跑的是显式的 OODA 循环(observe 观察 / orient 定位 / decide 决策 / act 行动),要求最少 5 次、最多 10 次工具调用。本质上就是 01 ReAct,只是把「想什么」结构化了。
3.5 主管的三条硬规则
① 收益递减就立刻停
当进一步研究已经收益递减、你已经能给出足够好的答案时,停止进一步研究,不要创建任何新子代理。
Deep Research 最大的成本黑洞就是「再查一轮说不定更全」。这条规则把「什么时候够了」显式交给主管判断。
② 绝不让子代理写最终报告
绝不创建子代理来生成最终报告 —— 你自己写,永远不允许用子代理来创建报告。
为什么?因为只有主管看过全部子代理的结果。 派个子代理去写报告,它拿到的是二手摘要,信息要损耗两次。
③ 主管不做主要研究
你的主要角色是协调、指导和综合 —— 不是自己做主要研究。
只有当某个关键问题子代理没覆盖到时,主管才自己动手。
②和③合起来是一条完整的分工原则:
主管不查资料,但必须自己写结论。
3.6 引用为什么要单独一个 Agent
citations_agent.md 只有 2.9KB,职责单一:给已经写好的报告逐句挂来源。
为什么不让主管边写边挂?因为「写得流畅」和「挂得准确」是两个互相拉扯的目标。混在一起,模型会为了行文顺畅而牺牲引用精度 —— 该标的地方不标,或者标一个大概相关的。
这和 04 Reflection 里「生成和批判要分开」是完全相同的道理:一次只让模型追求一个目标。
四、拆代码骨架:open_deep_research
官方还公开了完整生命周期图,可与下面的六节点代码骨架对读:

图 6-2 从用户提问、主管迭代、子代理并行检索、综合,到 CitationAgent 挂引用的全流程
图片来源:Anthropic — How we built our multi-agent research system
提示词讲完,看代码怎么组织。拆 langchain-ai/open_deep_research(12,636★,MIT)的 deep_researcher.py,30KB,六个节点:
async def clarify_with_user(...) # ① 需要跟用户确认吗
async def write_research_brief(...) # ② 把对话压成研究简报
async def supervisor(...) # ③ 主管决策
async def supervisor_tools(...) # 主管派活的执行
async def researcher(...) # ④ 子研究员 ReAct 循环
async def researcher_tools(...)
async def compress_research(...) # ⑤ 压缩
async def final_report_generation(...) # ⑥ 写报告
4.1 三条独立的消息通道
这是状态设计里最值得学的一点:
class AgentState(MessagesState):
supervisor_messages: ... # 主管的对话
research_brief: Optional[str]
raw_notes: list[str] = [] # 原始材料
notes: list[str] = [] # 压缩后的
class ResearcherState(TypedDict):
researcher_messages: ... # 子研究员自己的对话
compressed_research: str
用户对话、主管对话、子研究员对话是三份完全独立的消息历史。
这是 05 里「上下文隔离」的彻底版本 —— 不是靠 output_mode 事后裁剪,而是从状态结构上就分开。
raw_notes 和 notes 也是一对:前者存原始材料,后者存压缩结果。写报告时用 notes,需要溯源时查 raw_notes。
4.2 write_research_brief:被低估的一步
用户的原话是口语的、模糊的、带隐含前提的。直接把对话丢给主管派活,子代理拿到的任务描述会继承这份模糊。
这个节点的作用,是把对话固化成一份书面任务书,后面所有子代理都以它为准。
对应 3.3 说的「任务描述七要素」—— 简报本身写不清楚,七要素就无从谈起。
4.3 compress_research:实现方式很巧妙
# 在子研究员自己的消息历史后面,追加一条「现在切换到压缩模式」的指令
researcher_messages.append(HumanMessage(content=compress_research_simple_human_message))
compression_prompt = compress_research_system_prompt.format(date=get_today_str())
messages = [SystemMessage(content=compression_prompt)] + researcher_messages
response = await synthesizer_model.ainvoke(messages)
注意它不是新开一个 Agent 来读摘要,而是让子研究员自己切换到压缩模式。
差别在哪?
| 做法 | 压缩者看到的 |
|---|---|
| 新开一个 Agent | 只有子研究员交出来的结果 |
| 切换模式(这个实现) | 全部研究过程,包括试错、被否定的线索 |
看得越全,压缩质量越高。
还有一个实际的成本优化:压缩用的是单独配置的模型(configurable.compression_model)。压缩是个便宜活,没必要用最贵的模型。
外面还包了 3 次重试,专门处理压缩时撞上 token 上限的情况。
4.4 并发上限:超出的不丢弃,而是回一条错误
这段是全文件最值得学的:
allowed = conduct_research_calls[:configurable.max_concurrent_research_units]
overflow = conduct_research_calls[configurable.max_concurrent_research_units:]
tool_results = await asyncio.gather(*research_tasks)
# 超出的部分,每个都回一条错误消息
for overflow_call in overflow:
ToolMessage(
content=f"Error: Did not run this research as you have already exceeded the maximum "
f"number of concurrent research units. Please try again with "
f"{configurable.max_concurrent_research_units} or fewer research units.",
tool_call_id=overflow_call["id"],
)
主管一口气派了 12 个,系统上限 5 个 —— 前 5 个正常跑,后 7 个各回一条「你派太多了,请用 5 个或更少再试」。
这个模式在本专题里已经是第三次出现了:
| 出处 | 场景 | 做法 |
|---|---|---|
| 03 | 并行调了两次清单工具 | 回一条错误消息,让模型改成单次 |
| 05 | 并行交接产生非法历史 | 清理掉不属于该分支的调用 |